﻿namespace TfsReportRenderer
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Security.Cryptography;
    using System.Text;
    using System.Text.RegularExpressions;

    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.TestManagement.Client;
    using Microsoft.TeamFoundation.VersionControl.Client;

    /// <summary>
    /// The TFS context provides handling of the TFS data by requesting the 
    /// data from the service and transforming it into a TFS-independent data 
    /// structure.
    /// </summary>
    public class TfsContext
    {
        /// <summary>
        /// The URI to access the correct TFS project collection.
        /// </summary>
        private readonly Uri uri;

        /// <summary>
        /// Initializes a new instance of the <see cref="TfsContext"/> class.
        /// </summary>
        /// <param name="uri">The URI to the TFS project collection.</param>
        public TfsContext(Uri uri)
        {
            this.uri = uri;
        }

        /// <summary>
        /// Gets a list of team projects.
        /// </summary>
        /// <value> The names of the team projects. </value>
        public IEnumerable<string> TeamProjects
        {
            get
            {
                using (var tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(this.uri))
                {
                    var service = tfs.GetService<VersionControlServer>();
                    return service.GetAllTeamProjects(true).Select(i => i.Name).ToList();
                }
            }
        }

        /// <summary>
        /// Gets a list of test plans for a specific team project.
        /// </summary>
        /// <param name="teamProject"> The team project to get the plans for. </param>
        /// <returns> The names of the test plans. </returns>
        public IEnumerable<string> GetTestPlans(string teamProject)
        {
            return this.WithTestProject(teamProject, project => project.TestPlans.Query("SELECT * FROM TestPlan").Select(i => i.Name).ToList());
        }

        /// <summary>
        /// Gets the report data from the test results.
        /// </summary>
        /// <param name="projectName"> The name of the team project to work with. </param>
        /// <param name="planName"> The name of the test plan to open. </param>
        /// <param name="date">The point in time which is the maximum date for test runs to be taken into account.</param>
        /// <returns> The a list of data. </returns>
        public IEnumerable<TestResultData> GetReportData(string projectName, string planName, DateTime? date)
        {
            return this.WithTestProject(
                projectName,
                project =>
                {
                    int planId;
                    int.TryParse(planName, NumberStyles.Any, CultureInfo.InvariantCulture, out planId);
                    var plan = project
                        .TestPlans
                        .Query("SELECT * FROM TestPlan")
                        .FirstOrDefault(tp => planId > 0 ? tp.Id == planId : tp.Name == planName);

                    var scoreboards = new List<TestResultData>();
                    if (plan == null)
                    {
                        Console.WriteLine("Plan " + planName + " cannot be found.");
                        return scoreboards;
                    }

                    var points = plan.QueryTestPoints("SELECT * FROM TestPoint");

                    // A Test Point is a combination of Test Case and Configuration executed in the context of a Test Plan
                    // so we can extract all Test Points from the Suites contained in a Test Plan.
                    foreach (var point in points.OrderBy(x => x.SuiteId))
                    {
                        // for some test points we might have no results yet
                        var result = GetTestCaseResult(point, date, project);
                        if (result == null)
                        {
                            continue;
                        }

                        var suite = project.TestSuites.Find(point.SuiteId);
                        var testRun = project.TestRuns.Find(result.TestRunId);
                        var actions = point.TestCaseWorkItem.Actions;

                        var iterations = result.Iterations.ToArray();

                        if (iterations.Length == 0)
                        {
                            var dataRow = CreateTestResultDataRow(projectName, planName, suite, point, result);
                            dataRow.TestCaseStepName = "no steps have been executed";
                            dataRow.TestCaseStepOutcome = "no steps have been executed";
                            dataRow.TestCaseStepOutcomeComment = "no steps have been executed";
                            scoreboards.Add(dataRow);
                        }

                        var hasActions = false;
                        foreach (var iteration in iterations)
                        {
                            // now create a row for each step in a test case and show result
                            foreach (var testAction in actions)
                            {
                                hasActions = true;
                                var stepRows = this.CreateStepRow(projectName, planName, suite, point, result, iteration, testAction, testRun.Owner.DisplayName);

                                scoreboards.AddRange(stepRows);
                            }

                            if (hasActions)
                            {
                                continue;
                            }

                            var scoreboardrow = CreateTestResultDataRow(projectName, planName, suite, point, result);
                            scoreboardrow.TestIterationId = iteration.IterationId;
                            scoreboards.Add(scoreboardrow);
                        }
                    }

                    return scoreboards;
                });
        }

        /// <summary>Gets the test case result for a test point.</summary>
        /// <param name="point">The test point point.</param>
        /// <param name="date">The maximum date to get.</param>
        /// <param name="project">The team project.</param>
        /// <returns>The <see cref="ITestCaseResult"/>.</returns>
        private static ITestCaseResult GetTestCaseResult(ITestPoint point, DateTime? date, ITestManagementTeamProject project)
        {
            return date == null
                       ? point.MostRecentResult
                       : project
                             .TestResults
                             .ByTestId(point.TestCaseId)
                             .Where(x => x.DateCreated <= date)
                             .OrderByDescending(x => x.DateCreated)
                             .FirstOrDefault(testResult => testResult.TestPointId == point.Id);
        }

        /// <summary>
        /// Removes HTML tag information from the text.
        /// </summary>
        /// <param name="value"> The text. </param>
        /// <returns> The plain text. </returns>
        private static string StripHtml(string value)
        {
            return Regex.Replace(value.Replace("<BR />", " "), "\\<[^>]*\\>", string.Empty);
        }

        /// <summary>
        /// Creates a test result data row.
        /// </summary>
        /// <param name="teamProject"> The team project. </param>
        /// <param name="selectedTestPlan"> The selected test plan. </param>
        /// <param name="testSuite"> The test suite. </param>
        /// <param name="testPoint"> the test point. </param>
        /// <param name="testCaseResult"> The test case result. </param>
        /// <returns> The <see cref="TestResultData"/>. </returns>
        private static TestResultData CreateTestResultDataRow(string teamProject, string selectedTestPlan, ITestSuiteBase testSuite, ITestPoint testPoint, ITestResult testCaseResult)
        {
            return new TestResultData
                       {
                           TeamProject = teamProject,
                           TestPlan = selectedTestPlan,
                           Id = testSuite.Id,
                           TestSuiteName = testSuite.Title,
                           TestCaseName = testPoint.TestCaseWorkItem.Title,
                           TestCaseOutcome = StripHtml(testPoint.MostRecentResultOutcome.ToString()),
                           TestIterationDate = testCaseResult.DateCompleted,
                       };
        }

        /// <summary>
        /// Download an attachment.
        /// </summary>
        /// <param name="attachment"> The attachment to download. </param>
        /// <returns> The <see cref="Stream"/> of the downloaded content. </returns>
        private static Stream DownloadAttachment(ITestAttachment attachment)
        {
            if (attachment == null)
            {
                return null;
            }

            using (var downloader = new System.Net.WebClient())
            {
#if DEBUG
                // you might encrypt your credentials here and then set the environment variable TFSPWD
                // please write the password into the code, but simply set a breakpoint, change the content 
                // of the variable and then execute the debug-line.
                var pwd = "my good password" + string.Empty;
                Debug.Print(Convert.ToBase64String(ProtectedData.Protect(Encoding.ASCII.GetBytes(pwd), Encoding.ASCII.GetBytes("sem"), DataProtectionScope.CurrentUser)));
#endif

                var encpwd = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User)["TFSPWD"];
                var user = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User)["TFSUSR"];
                var domain = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User)["TFSDOM"];
                if (encpwd != null && user != null && domain != null)
                {
                    downloader.Credentials = new NetworkCredential(user.ToString(), Encoding.ASCII.GetString(ProtectedData.Unprotect(Convert.FromBase64String(encpwd.ToString()), Encoding.ASCII.GetBytes("sem"), DataProtectionScope.CurrentUser)), domain.ToString());
                }
                else
                {
                    downloader.Credentials = CredentialCache.DefaultNetworkCredentials;
                }

                var data = downloader.DownloadData(attachment.Uri);
                return new MemoryStream(data);
            }
        }

        /// <summary>
        /// Creates step rows - for a simple test action we will get one single row, for 
        /// a <see cref="ISharedStepReference"/> we may get multiple rows, because a shared
        /// step typically contains sub-steps.
        /// </summary>
        /// <param name="project"> The team project to use. </param>
        /// <param name="plan"> The test plan to get the data from. </param>
        /// <param name="suite"> The test suite of the test step. </param>
        /// <param name="testPoint"> The test point. </param>
        /// <param name="caseResult"> The test case result. </param>
        /// <param name="iteration"> The iteration. </param>
        /// <param name="testAction"> The test action. </param>
        /// <param name="owner">The owner of the test run.</param>
        /// <returns> One or more instances of <see cref="TestResultData"/>. </returns>
        private IEnumerable<TestResultData> CreateStepRow(string project, string plan, ITestSuiteBase suite, ITestPoint testPoint, ITestCaseResult caseResult, ITestIterationResult iteration, ITestAction testAction, string owner)
        {
            var row = CreateTestResultDataRow(project, plan, suite, testPoint, caseResult);
            row.TestIterationId = iteration.IterationId;

            var sharedStepsRef = testAction as ISharedStepReference;
            if (sharedStepsRef != null)
            {
                var sharedStep = sharedStepsRef.FindSharedStep();
                return sharedStep.Actions.SelectMany(a => this.CreateStepRow(project, plan, suite, testPoint, caseResult, iteration, a, owner));
            }

            var testStep = testAction as ITestStep;
            if (testStep != null)
            {
                var title = StripHtml(testStep.Title);
                if (testStep.Title.ParameterNames.Count > 0)
                {
                    var parameters = iteration.Parameters;

                    // we have aparameters, so replace them so we see actual values
                    // of the parameters for each iteration
                    foreach (var param in testStep.Title.ParameterNames)
                    {
                        var paramvalue = parameters.ContainsKey(param) ? parameters[param].ExpectedValue : null;
                        title = title.Replace("@" + param, string.Format(CultureInfo.InvariantCulture, "@{0}:'{1}'", param, paramvalue));
                    }
                }

                row.TestCaseStepName = title;

                var actionresult = iteration.Actions.OfType<ITestStepResult>().FirstOrDefault(x => x.ActionId == testAction.Id) as ITestActionResult
                                ?? iteration.Actions.OfType<ISharedStepResult>().SelectMany(x => x.Actions).OfType<ITestStepResult>().FirstOrDefault(x => x.ActionId == testAction.Id);

                if (actionresult == null)
                {
                    throw new InvalidOperationException("The StepResult of the shared step action cannot be resolved - please check your test design to not use deep nesting of shared steps.");
                }

                var attachment = actionresult.Attachments.FirstOrDefault(x => x.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase));
                if (attachment != null)
                {
                    row.Image = DownloadAttachment(attachment);
                }

                row.TestCaseStepOutcome = actionresult.Outcome.ToString();
                row.TestCaseStepOutcomeComment = actionresult.ErrorMessage;
                row.TestUser = owner;
            }

            return new[] { row };
        }

        /// <summary>
        /// Executes function with a parameter of type <see cref="ITestManagementTeamProject"/>.
        /// Since the <see cref="TfsTeamProjectCollection"/> that is needed to work with a 
        /// <see cref="ITestManagementTeamProject"/> needs to be disposed, does this function
        /// provide a way to build the <see cref="ITestManagementTeamProject"/> before and 
        /// dispose the object after the work.
        /// </summary>
        /// <param name="teamProject"> The team project. </param>
        /// <param name="func"> The function to execute. </param>
        /// <typeparam name="T"> The return type of the function. </typeparam>
        /// <returns> The result of the function. </returns>
        private T WithTestProject<T>(string teamProject, Func<ITestManagementTeamProject, T> func)
        {
            using (var tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(this.uri))
            {
                var service = tfs.GetService<ITestManagementService>();
                var testProject = service.GetTeamProject(teamProject);
                return func(testProject);
            }
        }
    }
}